<?php
/*
header block
    total:4 bytes
    0~3 bytes:record counts(little endian)
data block
index block
    total:3*record_counts bytes
    3 byte per block(little endian)
    each index records the file offset of the end of a data block.
*/
class SFL {
    private $mCount=false;
    private $mFile;
    private $mHandler;
    public function __construct($file) {
        if (!file_exists($file)) {
            $this->create($file);
        }
        $this->mFile = $file;
        $this->mHandler=fopen($file,'rb');
    }
    public function get($num,$offset=0) {
        $c=$this->count();
        if ($offset<0) {
            $offset=$c+$offset;
        }
        if ($offset<0) {
            if ($num>$offset) {
                $num=$num+$offset;
                $offset=0;
            } else {
                return array();
            }
        }
        if ($c<=$offset) {
            return array();
        }
        fseek($this->mHandler,$this->getIndexPos()+($offset-1)*3,SEEK_SET);
        if ($offset==0) {
            $start_pos=4;
            fseek($this->mHandler,3,SEEK_CUR);
        } else {
            $p=fread($this->mHandler,3);
            /*one's pos is the pre one's pos + 1*/
            $start_pos = current(unpack('V',$p.chr('0')))+1;
        }
        $left_c=$c-$offset;
        if ($left_c<$num) {
            $num=$left_c;
        }
        $indexes=fread($this->mHandler,$num*3);
        $lens = array();
        $total_len = 0;
        $pre_pos=$start_pos-1;
        for ($i=0;$i<$num;$i++) {
            $p=$indexes[$i*3].$indexes[$i*3+1].$indexes[$i*3+2];
            $c_pos = current(unpack('V',$p.chr(0)));
            $len=$c_pos-$pre_pos;
            $pre_pos=$c_pos;
            $lens[]=$len;
            $total_len+=$len;
        }
        fseek($this->mHandler,$start_pos,SEEK_SET);
        if ($total_len==0) {
            $data='';
        } else {
            $data=fread($this->mHandler,$total_len);
        }
        $results = array();
        $read_pos=0;
        foreach ($lens as $len) {
            if ($len==0) {
                $results[] = ''; 
            } else {
                $results[] = substr($data,$read_pos,$len);
                $read_pos+=$len;
            }
        }
        return $results;
    }
    public function getLatest($num,$offset=0) {
        return array_reverse($this->get($num,-($num+$offset)));
    }
    public function getRand($num) {
        $c=$this->count();
        $offsets = array();
        $results = array();
        if ($num>=$c) {
            $results = $this->get($c);
            shuffle($results);
        } else {
            while (count($offsets)<$num) {
                $offset=rand(0,$c-1);
                if (in_array($offset,$offsets)) {
                    continue;
                }
                $offsets[]=$offset;
                $results[]=current($this->get(1,$offset));
            }
        }
        return $results;
    }
    public function count() {
        if (false!==$this->mCount) {
            return $this->mCount;
        }
        fseek($this->mHandler,0,SEEK_SET);
        $s=fread($this->mHandler,4);
        if (strlen($s)<4) {
            $this->mCount=0;
        } else {
            $this->mCount = current(unpack('V',$s));
        }
        return $this->mCount;
    }
    public function add($str) {
        fclose($this->mHandler);
        $this->mHandler=fopen($this->mFile,'r+b');
        $i_pos=$this->getIndexPos();
        $c=$this->count();
        $l=strlen($str);
        $i_pos+=$l;

        fseek($this->mHandler,0,SEEK_SET);
        fwrite($this->mHandler,pack('V',$c+1));

        if ($c<=0) {
            $index_data='';
        } else {
            fseek($this->mHandler,-$c*3,SEEK_END);
            $index_data=fread($this->mHandler,$c*3);
        }
        /*
            file offset of ending of the last data is (first index position - 1)
        */
        $index_data.=substr(pack('V',$i_pos-1),0,3);

        fseek($this->mHandler,-$c*3,SEEK_END);
        fwrite($this->mHandler,$str.$index_data);
        $this->flush();
    }
    public function delete($str) {
        fclose($this->mHandler);
        $this->mHandler=fopen($this->mFile,'r+b');

        while (false!==($delete_i_pos=$this->find($str))) {
            $i_pos=$this->getIndexPos();
            $c=$this->count();

            fseek($this->mHandler,0,SEEK_SET);
            fwrite($this->mHandler,pack('V',$c-1));

            if ($delete_i_pos==$i_pos) {
                fseek($this->mHandler,$delete_i_pos,SEEK_SET);
                $delete_pos_start=4;
            } else {
                fseek($this->mHandler,$delete_i_pos-3,SEEK_SET);
                $delete_pos_start=current(unpack('V',fread($this->mHandler,3).chr(0)))+1;
            }
            $delete_pos_end=current(unpack('V',fread($this->mHandler,3).chr(0)));
            fseek($this->mHandler,$delete_pos_end+1,SEEK_SET);
            $pos_reduce=$delete_pos_end-$delete_pos_start+1;
            if (($delete_i_pos-$delete_pos_end-1)>0) {
                $no_change_data=fread($this->mHandler,$delete_i_pos-$delete_pos_end-1);
                fseek($this->mHandler,3,SEEK_CUR);
                $need_change_size=filesize($this->mFile)-$delete_i_pos-3;
                $need_change_data='';
                if ($need_change_size>0) {
                    $need_change_data=fread($this->mHandler,$need_change_size);
                    $n=strlen($need_change_data)/3;
                    for ($i=0;$i<$n;$i++) {
                        $new_pos=current(unpack('V',$need_change_data[$i*3].$need_change_data[$i*3+1].$need_change_data[$i*3+2].chr(0)))-$pos_reduce;
                        $new_pos=pack('V',$new_pos);
                        $need_change_data[$i*3]   =  $new_pos[0];
                        $need_change_data[$i*3+1] =  $new_pos[1];
                        $need_change_data[$i*3+2] =  $new_pos[2];
                    }
                }
                fseek($this->mHandler,$delete_pos_start,SEEK_SET);
                fwrite($this->mHandler,$no_change_data.$need_change_data);
            }
            ftruncate($this->mHandler,filesize($this->mFile)-3-$pos_reduce);
            $this->flush();
            break;
        }
    }
    public function exist($str) {
        return (bool)$this->find($str);
    }
    private function find($str) {
        $num=$this->count();
        if ($num==0) {
            return false;
        }
        $start_pos=4;
        $i_pos=$this->getIndexPos();
        fseek($this->mHandler,$i_pos);
        $indexes=fread($this->mHandler,$num*3);
        $pre_pos=$start_pos-1;
        fseek($this->mHandler,$start_pos,SEEK_SET);
        for ($i=0;$i<$num;$i++) {
            $p=$indexes[$i*3].$indexes[$i*3+1].$indexes[$i*3+2];
            $c_pos = current(unpack('V',$p.chr(0)));
            $len=$c_pos-$pre_pos;
            $pre_pos=$c_pos;
            if ($len<=0) { 
                $test_str='';
            } else {
                $test_str=fread($this->mHandler,$len);
            }
            if ($test_str==$str) {
                return $i_pos+$i*3;
            }
        }
        return false;
    }
    public function create($file) {
        touch($file);
        $this->mHandler=fopen($file,'r+b');
        fwrite($this->mHandler,pack('V',0));
        $this->flush();
    }
    public function getIndexPos() {
        fseek($this->mHandler,0,SEEK_SET);
        $h=fread($this->mHandler,4);
        return filesize($this->mFile)-current(unpack('V',$h))*3;
    }
    public function flush() {
        $this->mCount=false;
        clearstatcache();
    }
    public function __destruct() {
        fclose($this->mHandler);
    }
}
